/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package network;

import helper.Config;
import java.util.ArrayList;
import java.util.Collection;
import java.util.logging.Level;
import java.util.logging.Logger;
import schedulingScheme.*;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;

/**
 *
 * @author Andrzej
 */
public class Leach implements Network {

    private List<NetworkListener> actionListeners = new ArrayList<NetworkListener>();
    private List<SensorNode> sensors = new ArrayList<SensorNode>();
    private Map<SensorNode, Integer> whenCH = new HashMap<SensorNode, Integer>();
    private final Map<SensorNode, Set<SensorNode>> CHs = new HashMap<SensorNode, Set<SensorNode>>();
    private SensorNode sink;
   
    private double p = 0.1;
    private int invertedP = 10;
    int r = 1; //round number
    int time = 0;
    Random random = Config.getSalt();
    private double compressionRatio = 0.5;

    public Leach() {
        for (SensorNode sn : sensors) {
            this.sensors.add(sn);
        }
    }

    public void phase1() {
        CHs.clear();
        CHs.put(sink, new HashSet<SensorNode>());//sink allways is a CH
        if (r % invertedP == 0) {
            whenCH.clear();
        }
        for (SensorNode sn : sensors) {
            if (!whenCH.containsKey(sn)) {
                double T = p / (1 - p * (r % invertedP));//threshold
                if (r % invertedP == invertedP - 1) {
                    T = 1;
                }
                if (random.nextDouble() < T) {
                    whenCH.put(sn, r);
                    CHs.put(sn, new HashSet<SensorNode>());
                    //ustaw CH
                }
            }
        }
        //sensors that are not CH are joining nearest CH
        for (SensorNode s : sensors) {
            if (CHs.containsKey(s)) {
                continue;
            }
            SensorNode closestCH = findClosestSensorNode(s, CHs.keySet());
            CHs.get(closestCH).add(s);
        }


    }

    public void phase2() {


        for (SensorNode ch : CHs.keySet()) {
            try {
                if (!ch.isOn()) {
                    continue;
                }
                for (SensorNode s : CHs.get(ch)) {
                    if (!s.isOn()) {
                        continue;
                    }
                    try {
                        s.sendMessage(ch, MESSAGE_LENGTH);

                    } catch (BatteryExhaustedException ex) {
                        reportSensorDied(s);
                        sensors.remove(s);
                        System.out.println("Sensor: " + ex.getSensorNode() + " died");
                    }
                    ch.receiveMessage(MESSAGE_LENGTH);
                }
                ch.sendMessage(sink, (int) (MESSAGE_LENGTH * (CHs.get(ch).size() + 1) * compressionRatio));

            } catch (BatteryExhaustedException ex) {
//                Logger.getLogger(Leach.class.getName()).log(Level.SEVERE, null, ex);
//                System.out.println("Cluster head: " + ex.getSensorNode() + " died");
                reportSensorDied(ch);
//                CHs.remove(ch);
                sensors.remove(ch);

            }
        }

    }

    private void reportSensorDied(SensorNode s) {
        for (NetworkListener nl : actionListeners) {
            nl.sensorDied(s, time);
        }
    }

    private void reportNewRound() {
        for (NetworkListener nl : actionListeners) {
            nl.newRound(r, time);
        }
    }

    private void reportNetworkReorganized() {
        for (NetworkListener nl : actionListeners) {
            nl.networkReorganized(r, time);
        }
    }
    private void reportTimePassed(int time) {
        for (NetworkListener nl : actionListeners) {
            nl.timePassed(time);
        }
    }

    private SensorNode findClosestSensorNode(SensorNode s, Collection<SensorNode> l) {
        SensorNode closestNode = null;
        double distanceOfClosestNode = 100000000;//very large number - infinity
        for (SensorNode sn : l) {
            double distance = sn.distance(s);
            if (distance < distanceOfClosestNode) {
                closestNode = sn;
                distanceOfClosestNode = distance;
            }
        }
        return closestNode;
    }

    public void addSensor(SensorNode s) {
        sensors.add(s);//it will be activated when round pass
    }

    public void removeSensor(SensorNode s) {
        if (r % invertedP != 0)
            throw new IllegalStateException("You can't remove sensor at this round");
        sensors.remove(s);
    }

    public void start() {

        while (sensors.size() > 0) {
            reportNewRound();
            synchronized (CHs) {//because MetworkInfo is used in AWT-Thread
                phase1();
            }
            reportNetworkReorganized();
            for (int j = 0; j < ROUND_LENGTH; j++) {
                phase2();
                time++;
                reportTimePassed(time);
            }
            r++;

        }
        System.out.println("" + time);
    }

    public void addNetworkListener(NetworkListener nl) {
        actionListeners.add(nl);
    }

    public void addSink(SensorNode s) {
        sink = s;
    }

    public NetworkInfo getNetworkInfo() {
        return new NetworkInfo() {

            Map<SensorNode, Collection<POI>> poisInRange = new HashMap<SensorNode, Collection<POI>>();

            public int getSensorRole(SensorNode s) {
                synchronized(CHs){
                if (CHs.containsKey(s)) {
                    return CLUSTER_HEAD;
                } else {
                    for (SensorNode ch : CHs.keySet()) {
                        if (CHs.get(ch).contains(s)) {
                            return REGULAR_NODE;
                        }
                    }
                }
                return NOT_IN_NETWORK;
                }
            }

            public Collection<SensorNode> getSensorsInCluster(SensorNode clusterHead) {
                return CHs.get(clusterHead);
            }

            public Collection<SensorNode> activeSensors() {
                synchronized(CHs){
                List<SensorNode> res = new ArrayList<SensorNode>();
                for (SensorNode ch : CHs.keySet()) {
                    if (! ch.isOn()){
                        continue;
                    }
                    if (ch != sink) {
                        res.add(ch);
                    }
                    for (SensorNode s : CHs.get(ch)) {
                        if(s.isOn())
                            res.add(s);
                    }
                }
                return res;
            }
            }
        };

    }
}
